let TRANSFER_THRESHOLD = 64;
class USBError extends Error {
    code;
    constructor(code, msg) {
        super(msg);
        this.code = code;
        this.name = this.constructor.name;
    }
}
class InvalidStateError extends USBError {
    constructor(msg) {
        super(11, msg);
    }
}

class NotFoundError extends USBError {
    constructor(msg) {
        super(8, msg);
    }
}
class NoPermissionError extends USBError {
    constructor(msg) {
        super(9, msg);
    }
}

class NetworkError extends USBError {
    constructor(msg) {
        super(19, msg);
    }
}
const reqTypeMap = {
    standard: 0x00,
    class: 0x20,
    vendor: 0x40,
    device: 0x00,
    interface: 0x01,
    endpoint: 0x02,
    other: 0x03
}

class USBDevice {
    connected;
    opened = false;
    #convertHarmonyToWebUSB(harmonyDevice) {
        try {
            const configurations = this.#convertConfigurations(harmonyDevice.configs);
            return {
                usbVersionMajor: parseInt(harmonyDevice.version.slice(2)),
                usbVersionMinor: parseInt(harmonyDevice.version.slice(0, 2)),
                usbVersionSubminor: 0,

                deviceClass: harmonyDevice.clazz,
                deviceSubclass: harmonyDevice.subClass,
                deviceProtocol: harmonyDevice.protocol < 0 ? 256 + harmonyDevice.protocol : harmonyDevice.protocol,

                vendorId: harmonyDevice.vendorId,
                productId: harmonyDevice.productId,
                deviceVersionMajor: 0,
                deviceVersionMinor: 0,
                deviceVersionSubminor: 0,

                manufacturerName: harmonyDevice.manufacturerName,
                productName: harmonyDevice.productName,
                serialNumber: harmonyDevice.serial,
                configurations,
                configuration: configurations[0],
                opened: false,
                //鸿蒙内的专有属性
                name: harmonyDevice.name,
                busNum: harmonyDevice.busNum,
                deviceAddress: harmonyDevice.devAddress,
            };
        } catch (error) {
            console.error('设备转换失败:', error);
            return null;
        }
    }

    #convertConfigurations(configs) {
        return configs.map(config => ({
            configurationValue: config.id,
            configurationName: config.name,
            interfaces: this.#convertInterfaces(config.interfaces),
        }));
    }

    #convertInterfaces(interfaces) {
        const interfaceMap = new Map();
        interfaces.forEach(intf => {
            if (!interfaceMap.has(intf.id)) {
                interfaceMap.set(intf.id, {
                    interfaceNumber: intf.id,
                    claimed: false,
                    alternates: []
                });
            }
            const infce = interfaceMap.get(intf.id);
            const alternate = {
                alternateSetting: intf.alternateSetting,
                interfaceClass: intf.clazz,
                interfaceSubclass: intf.subClass,
                interfaceProtocol: intf.protocol,
                interfaceName: intf.name,
                endpoints: this.#convertEndpoints(intf.endpoints)
            };
            // 添加备用设置
            infce.alternates.push(alternate);
            if (!infce.alternate) {
                infce.alternate = alternate;
            }
        });
        return Array.from(interfaceMap.values());
    }

    #convertEndpoints(endpoints) {
        const endpointTypes = ["control", "isochronous", "bulk", "interrupt"];
        return endpoints.map(endpoint => ({
            endpointNumber: endpoint.number,
            direction: endpoint.direction == 0x00 ? "out" : "in",
            //"bulk" | "interrupt" | "isochronous","";
            type: endpointTypes[endpoint.type] || "unknown",
            packetSize: endpoint.maxPacketSize,
        }));
    }
    #connectedCheck(caller) {
        if (!this.connected) {
            throw new NotFoundError(`Failed to execute '${caller}' on 'USBDevice': The device was disconnected.`)
        }
    }
    #openCheck(caller) {
        if (!this.opened) {
            throw new InvalidStateError(`Failed to execute '${caller}' on 'USBDevice': The device was opened first.`)
        }
    }
    #configurationCheck(caller) {
        if (!this.configuration) {
            throw new NotFoundError(`Failed to execute '${caller}' on 'USBDevice': The device is not configured.`)
        }
    }
    #getInterface(interfaceNumber, caller) {
        this.#configurationCheck(caller);
        const infce = this.configuration.interfaces.find(intf => intf.interfaceNumber == interfaceNumber);
        if (!infce) {
            throw new NotFoundError(`Failed to execute '${caller}' on 'USBDevice': The interface number provided is not supported by the device in its current configuration.`);
        }
        return infce;
    }
    #getEndpoint(direction, endpointNumber, caller) {
        if (direction != "in" && direction != "out") {
            throw new TypeError(`Failed to execute 'clearHalt' on 'USBDevice': The provided value '${direction}' is not a valid enum value of type USBDirection.`);
        }
        this.#configurationCheck(caller);
        const endpoints = this.configuration.interfaces.filter(infce => infce.claimed)
            .flatMap(intf => intf.alternate.endpoints)
            .filter(endpoint => endpoint.direction == direction && endpoint.endpointNumber == endpointNumber);
        if (endpoints.length === 0) {
            throw new NotFoundError(`Failed to execute '${caller}' on 'USBDevice':  The specified endpoint is not part of a claimed and selected alternate interface.`);
        }
        const endpoint = endpoints[0];
        return {
            direction: endpoint.direction == "in" ? 0x80 : 0x00,
            endpointNumber: endpoint.endpointNumber,
        };
    }
    constructor(ohosDevice) {
        const deviceData = this.#convertHarmonyToWebUSB(ohosDevice);
        if (deviceData) {
            Object.assign(this, deviceData);
            this.connected = true;
        } else {
            throw new Error('Unable to create USB device object.')
        }
    }
    async open() {
        this.#connectedCheck('open');
        if (this.opened) {
            return;
        }
        try {
            await new Promise((r, j) => ohosUSB.openDevice(this.name, r, j));
            this.opened = true;
        } catch (e) {
            throw e;
        }
    }
    async close() {
        this.#connectedCheck('close');
        if (!this.opened) {
            return;
        }
        const promises = [];
        if (this.configuration) {
            this.configuration.interfaces.forEach(intf => {
                if (intf.claimed) {
                    promises.push(this.releaseInterface(intf.interfaceNumber));
                }
            });
            await Promise.all(promises);
        }
        try {
            await new Promise((r, j) => ohosUSB.closeDevice(this.name, r, j));
            this.opened = false;
        } catch (e) {
            throw e;
        }
    }
    async claimInterface(interfaceNumber) {
        this.#connectedCheck('claimInterface');
        this.#openCheck('claimInterface');
        const infce = this.#getInterface(interfaceNumber, 'claimInterface');
        if (infce.claimed) {
            return;
        }
        try {
            await new Promise((r, j) => {
                ohosUSB.claimDeviceInterface(this.name, interfaceNumber, infce.alternate.alternateSetting, r, j);
            });
            infce.claimed = true;
        } catch (e) {
            throw e;
        }
    }
    async releaseInterface(interfaceNumber) {
        this.#connectedCheck('releaseInterface');
        this.#openCheck('releaseInterface');
        const infce = this.#getInterface(interfaceNumber, 'releaseInterface');
        if (!infce.claimed) {
            return;
        }
        try {
            await new Promise((r, j) => ohosUSB.releaseDeviceInterface(this.name, interfaceNumber, r, j));
            infce.claimed = false;
        } catch (e) {
            throw e;
        }
    }
    async selectAlternateInterface(interfaceNumber, alternateSetting) {
        this.#connectedCheck('selectAlternateInterface');
        this.#openCheck('selectAlternateInterface');
        this.#configurationCheck('selectAlternateInterface');
        const infce = this.#getInterface(interfaceNumber, 'selectAlternateInterface');
        if (!infce.claimed) {
            throw new InvalidStateError(`Failed to execute 'selectAlternateInterface' on 'USBDevice': The interface number provided is not claimed.`);
        }
        if (infce.alternate.alternateSetting === alternateSetting) {
            return;
        }
        const alternate = infce.alternates.find(alternate => alternate.alternateSetting === alternateSetting);
        if (!alternate) {
            throw new NotFoundError(`Failed to execute 'selectAlternateInterface' on 'USBDevice': The alternate setting provided is not supported by the device in its current configuration.`);
        }
        try {
            await new Promise((r, j) => {
                ohosUSB.selectDeviceAlternateInterface(this.name, interfaceNumber, alternateSetting, r, j);
            });
            infce.alternate = alternate;
        } catch (error) {
            throw error;
        }
    }
    async selectConfiguration(configurationValue) {
        this.#connectedCheck('selectConfiguration');
        this.#openCheck('selectConfiguration');

        if (this.configuration && this.configuration.configurationValue === configurationValue) {
            return;
        }
        const cfg = this.configurations.find(cfg => cfg.configurationValue === configurationValue);
        if (!cfg) {
            throw new NotFoundError(`Failed to execute 'selectConfiguration' on 'USBDevice': The configuration value provided is not supported by the device.`);
        }
        const promises = [];
        this.configuration.interfaces.forEach(intf => {
            if (intf.claimed) {
                promises.push(this.releaseInterface(intf.interfaceNumber));
            }
        });
        await Promise.all(promises);
        try {
            await new Promise((r, j) => {
                ohosUSB.selectDeviceConfiguration(this.name, configurationValue, r, j);
            });
            this.configuration = cfg;
        } catch (error) {
            throw error;
        }
    }
    async forget() {
        this.#connectedCheck('forget');
        if (this.opened) {
            await this.close();
        }
        try {
            await new Promise((r, j) => ohosUSB.forgetDevice(this.name, r, j))
            this.connected = false;
            this.opened = false;
        } catch (e) {
            throw e;
        }
    }

    async clearHalt(direction, endpointNumber) {
        this.#connectedCheck('clearHalt');
        this.#openCheck('clearHalt');
        const endpoint = this.#getEndpoint(direction, endpointNumber, 'clearHalt');

        try {
            await new Promise((r, j) => ohosUSB.clearDeviceHalt(this.name, endpoint.direction, endpoint.endpointNumber, r, j));
        } catch (e) {
            throw e;
        }
    }
    async transferOut(endpointNumber, data) {
        this.#connectedCheck('transferOut');
        this.#openCheck('transferOut');
        const endpoint = this.#getEndpoint("out", endpointNumber, 'transferOut');
        try {
            const result = await new Promise((r, j) =>
                (!ohosUSB.taskMgr.msgPort) || (data.length < TRANSFER_THRESHOLD) ?
                    ohosUSB.transferDeviceOut(this.name, endpoint, data, r, j) :
                    ohosUSB.transferDeviceOut(this.name, endpoint, () => ohosUSB.taskMgr.msgPort.postMessage(data.buffer), r, j)

            );
            return result;
        } catch (e) {
            throw e;
        }
    }
    async transferIn(endpointNumber, length) {
        this.#connectedCheck('transferIn');
        this.#openCheck('transferIn');
        const endpoint = this.#getEndpoint("in", endpointNumber, 'transferIn');
        try {
            const result = await new Promise((r, j) =>
                (!ohosUSB.taskMgr.msgPort) || (length < TRANSFER_THRESHOLD) ?
                    ohosUSB.transferDeviceIn(this.name, endpoint, length, r, j, false) :
                    ohosUSB.transferDeviceIn(this.name, endpoint, length, _ => ohosUSB.taskMgr.pushTask(r), j, true)
            );
            if (result instanceof Array) {
                return {
                    data: new DataView(new Uint8Array(result).buffer),
                    status: "ok"
                }
            } else {
                return {
                    data: new DataView(result),
                    status: "ok"
                }
            }
        } catch (e) {
            throw e;
        }
    }
    #setupParamCheck(setup, caller) {
        const checklist = ["requestType", "recipient", "request", "value", "index"];
        const requestTypeEnum = ["standard", "class", "vendor"];
        const recipientEnum = ["device", "interface", "endpoint", "other"];

        checklist.forEach((item) => {
            if (setup[item] == undefined)
                throw new TypeError(`Failed to execute '${caller}' on 'USBDevice': Failed to read the '${item}' property from 'USBControlTransferParameters': Required member is undefined.`);
        })
        if (!requestTypeEnum.includes(setup.requestType)) {
            throw new typeError(`Failed to execute '${caller}' on 'USBDevice': Failed to read the 'requestType' property from 'USBControlTransferParameters': The provided value '${setup.requestType}' is not a valid enum value of type USBRequestType.`);
        }
        if (!recipientEnum.includes(setup.recipient)) {
            throw new typeError(`Failed to execute '${caller}' on 'USBDevice': Failed to read the 'recipient' property from 'USBControlTransferParameters': The provided value '${setup.recipient}' is not a valid enum value of type USBRecipient.`);
        }
    }
    async controlTransferIn(setup, length) {
        this.#connectedCheck('controlTransferIn');
        this.#openCheck('controlTransferIn');
        this.#setupParamCheck(setup, 'controlTransferIn');
        const request = {
            bmRequestType: reqTypeMap[setup.requestType] | reqTypeMap[setup.recipient] | 0x80,
            bRequest: setup.request,
            wValue: setup.value,
            wIndex: setup.index,
            wLength: length,
        }
        try {
            const result = await new Promise((r, j) => ohosUSB.controlDeviceTransferIn(this.name, request, r, j));
            return {
                data: new DataView(new Uint8Array(result).buffer),
                status: "ok"
            }
        } catch (e) {
            throw e;
        }
    }
    async controlTransferOut(setup, data) {
        this.#connectedCheck('controlTransferIn');
        this.#openCheck('controlTransferIn');
        this.#setupParamCheck(setup, 'controlTransferIn');
        const request = {
            bmRequestType: reqTypeMap[setup.requestType] | reqTypeMap[setup.recipient],
            bRequest: setup.request,
            wValue: setup.value,
            wIndex: setup.index,
            wLength: data ? data.length : 0,
        }
        try {
            const buffer = data ? Array.from(data) : [];
            const result = await new Promise((r, j) => ohosUSB.controlDeviceTransferOut(this.name, request, buffer, r, j));
            return {
                bytesWritten : result,
                status: "ok"
            }
        } catch (e) {
            throw e;
        }
    }
}

class USBConnectionEvent extends Event {
    constructor(type, device) {
        super(type);
        this.device = device;
    }
}
class WebUSBClient extends EventTarget {
    #devices = new Map();
    constructor() {
        super();
        ohosUSB.registEventCallback(this.#deviceChange.bind(this));
    }
    #deviceChange(event, ohosDevice) {
        switch (event) {
            case "connect":
                this.#handleDeviceConnected(ohosDevice);
                break;
            case "disconnect":
                this.#handleDeviceDisconnected(ohosDevice);
                break;
            case "forget":
                this.#handleForget(ohosDevice);
                break;
            default:
                console.warn("Unknown event type: " + event);
        }
    }
    #handleForget(ohosDevice) {
        this.#devices.delete(ohosDevice.name);
    }
    #handleDeviceConnected(ohosDevice) {
        const usbDevice = new USBDevice(ohosDevice);
        this.#devices.set(usbDevice.name, usbDevice);
        this.dispatchEvent(new USBConnectionEvent("connect", usbDevice));
    }
    #handleDeviceDisconnected(ohosDevice) {
        const usbDevice = this.#devices.get(ohosDevice.name);
        if (usbDevice) {
            usbDevice.connected = false;
            usbDevice.opened = false;
            this.#devices.delete(usbDevice.name);
            this.dispatchEvent(new USBConnectionEvent("disconnect", usbDevice));
        }
    }
    requestDevice(filters) {
        return new Promise((r, j) => {
            ohosUSB.requestDevice(filters, harmonyDevice => {
                if (this.#devices.has(harmonyDevice.name)) {
                    r(this.#devices.get(harmonyDevice.name))
                } else {
                    const device = new USBDevice(harmonyDevice);
                    this.#devices.set(harmonyDevice.name, device)
                    r(device);
                }
            }, j);
        });
    }
    setTransferThreshold(size) {
        if (size > 0) {
            TRANSFER_THRESHOLD = size;
        } else {
            console.warn("Invalid transfer threshold size");
        }
        return TRANSFER_THRESHOLD;
    }
    async getDevices() {
        return Array.from(this.#devices.values());
    }
}
class TaskQueue {
    taskList = [];
    msgPort;

    constructor() {
        this.taskList = [];
        window.onmessage = e => {
            if (e.data === "__init__messagePort") {
                this.msgPort = e.ports[0];
                this.msgPort.onmessage = this.processTask;
                this.msgPort.postMessage("OK");
            }
        }
    }
    pushTask(task) {
        this.taskList.push(task);
    }
    processTask = (data) => {
        if (this.taskList.length > 0) {
            const task = this.taskList.shift();
            task(data.data);
        }
    }
}
ohosUSB.taskMgr = new TaskQueue();
ohosUSB.initMessagePort();


Object.defineProperty(navigator, "usb", {
    value: new WebUSBClient(),
    enumerable: false,
    configurable: false,
    writable: false
});

window.usb = navigator.usb;
console.log("WebUSBClient.js loaded");